//! Disk layout:
//! [ boot block (not used) | superblock | log | inode blocks | free bit map | data blocks ]
//! 1 fs block = 1 disk sector

gpa: std.mem.Allocator,
file: std.fs.File,
size: u32,
bmap: BitSet,
/// the first free block that we can allocate
freeblock: u32,
freeinode: u16 = 1,

const Disk = @This();
const std = @import("std");
const log = std.log.scoped(.mkfs);
const Inode = @import("Inode.zig");
const BitSet = std.bit_set.DynamicBitSetUnmanaged;

const fs = @import("fs");
const bmapstart = @import("root").bmapstart;

pub fn init(gpa: std.mem.Allocator, path: []const u8, size: u32, nbmap: u32) !Disk {
    const f = try std.fs.cwd().createFile(path, .{ .read = true });

    // allocate a sparse file
    try f.seekTo(size * fs.BSIZE - 1);
    _ = try f.write("$");

    return .{
        .gpa = gpa,
        .file = f,
        .size = size,
        .bmap = try .initEmpty(gpa, fs.BPB * nbmap),
        .freeblock = bmapstart + nbmap,
    };
}

pub fn deinit(self: *Disk) void {
    self.commit() catch unreachable;
    self.bmap.deinit(self.gpa);
    self.file.close();
}

/// buf must be little endian data
pub fn write(self: Disk, sector: usize, offset: usize, buf: []const u8) !usize {
    if (sector >= self.size) return error.TOO_LARGE;
    try self.file.seekTo(sector * fs.BSIZE + offset);

    // make sure we don't write to the next sector
    const len = if (offset + buf.len > fs.BSIZE) fs.BSIZE - offset else buf.len;
    try self.file.writeAll(buf[0..len]);
    return len;
}

pub fn read(self: Disk, sector: usize, buf: *[fs.BSIZE]u8) !void {
    _ = try self.file.pread(buf, sector * fs.BSIZE);
}

/// write to bitmap
fn commit(self: *Disk) !void {
    log.info("first {} blocks have been allocated", .{self.freeblock});
    self.bmap.unsetAll();
    self.bmap.setRangeValue(.{ .start = 0, .end = self.freeblock }, true);

    const bsize = fs.BSIZE / @sizeOf(BitSet.MaskInt);

    for (0..(self.freeblock - 1) / fs.BPB + 1) |block_i| {
        const bmap = bmapstart + block_i;
        log.info("write bitmap block at sector {}", .{bmap});
        const bmap_block: *[fs.BSIZE]u8 = @ptrCast(self.bmap.masks[bsize * block_i ..][0..bsize]);
        _ = try self.write(bmap, 0, bmap_block);
    }
}

/// Recursively walks a directory tree, creating corresponding inodes and directory entries.
pub fn walk(disk: *Disk, dir: std.fs.Dir, node: *Inode, parent: *Inode) !void {
    // create standard directory entries
    try node.entry(parent.number, "..");
    try node.entry(node.number, ".");

    var iter = dir.iterate();
    while (try iter.next()) |entry| {
        var subnode: Inode = .alloc(disk, switch (entry.kind) {
            .directory => .dir,
            else => .file,
        });
        defer subnode.commit();

        const path = entry.name;
        try node.entry(subnode.number, path);

        switch (entry.kind) {
            .directory => {
                var subdir = try dir.openDir(path, .{ .iterate = true });
                defer subdir.close();
                try disk.walk(subdir, &subnode, node);
            },
            .file => {
                const app = try dir.openFile(path, .{});
                defer app.close();

                var buf: [fs.BSIZE]u8 = undefined;
                while (true) {
                    const amt = try app.read(&buf);
                    if (amt == 0) break;
                    try subnode.append(buf[0..amt]);
                }
            },
            else => unreachable,
        }
    }
}
